﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;

namespace Ginigo.Net.FtpClient
{
     public class FtpClient
    {
        #region Variables
        private static int BUFFER_SIZE = 512;
        private static Encoding ASCII = Encoding.ASCII;
        private string _FtpClientServer = string.Empty;
        private string _ftpCurrentPath = ".";
        private string _ftpUsername = string.Empty;
        private string _ftpPassword = string.Empty;
        private string statusMessage = string.Empty;
        private string result = string.Empty;
        private int _ftpPort = 21;
        private int bytesResponse= 0;
        private int _statusCode = 0;
        private bool _isLoggedIn = false;
        private bool _isBinary = false;
        private Byte[] buffer = new Byte[BUFFER_SIZE];
        private Socket ftpSocket = null;
        private int _timeOut = 10;
        #endregion

        #region FtpClient Properties
        #region Read/Write Properties
        /// <summary>
        /// FTP Server port to use, default is usually 21
        /// </summary>
        public int FtpPort
        {
            get { return _ftpPort; }
            set { _ftpPort = value; }
        }

        /// <summary>
        /// Timeout waiting for a response from server, in seconds.
        /// </summary>
        public int TimeoutValue
        {
            get { return _timeOut; }
            set { _timeOut = value; }
        }

        /// <summary>
        /// Name of the FTP server we wish to connect to
        /// </summary>
        /// <returns></returns>
        public string FtpClientServer
        {
            get { return _FtpClientServer; }
            set { _FtpClientServer = value; }
        }

        /// <summary>
        /// The remote port we wish to connect through
        /// </summary>
        /// <returns></returns>
        public int RemotePort
        {
            get { return _ftpPort; }
            set { _ftpPort = value; }
        }

        /// <summary>
        /// The working directory
        /// </summary>
        public string FtpCurrentPath
        {
            get { return _ftpCurrentPath; }
            set { _ftpCurrentPath = value; }

        }

        /// <summary>
        /// Server username
        /// </summary>
        public string FtpUsername
        {
            get { return _ftpUsername; }
            set { _ftpUsername = value; }
        }

        /// <summary>
        /// Server password
        /// </summary>
        public string FtpPassword
        {
            get { return _ftpPassword; }
            set { _ftpPassword = value; }
        }

        /// <summary>
        /// If the value of mode is true, set 
        /// binary mode for downloads, else, Ascii mode.
        /// </summary>
        public bool IsBinary
        {
            get { return _isBinary; }
            set
            {
                if (_isBinary == value) return;
                if (value)
                    ExecuteCommand("TYPE I");
                else
                    ExecuteCommand("TYPE A");
                if (_statusCode != 200)
                {
                    throw new FtpClienException(result.Substring(4));
                }
            }
        }
        #endregion

        #region ReadOnly Properties
        /// <summary>
        /// determine if the user is logged in
        /// </summary>
        public bool IsLoggedIn
        {
            get { return _isLoggedIn; }
        }

        /// <summary>
        /// returns the status code of the command
        /// </summary>
        public int StatusCode
        {
            get { return _statusCode; }
        }
        #endregion
        #endregion

        #region FtpClient Methods
        #region FtpCLientLogin
        /// <summary>
        /// method to log in to the remote ftp server
        /// </summary>
        public void FtpClientLogin()
        {
            if (_isLoggedIn)
            {
                CloseConnection();
            }
            IPAddress remoteAddress = null;
            IPEndPoint addrEndPoint = null;
            try
            {
                ftpSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
                remoteAddress = Dns.GetHostEntry(_FtpClientServer).AddressList[0];
                addrEndPoint = new IPEndPoint(remoteAddress, _ftpPort);
                ftpSocket.Connect(addrEndPoint);
            }
            catch (Exception ex)
            {
                if (ftpSocket != null && ftpSocket.Connected)
                {
                    ftpSocket.Close();
                }
                throw new FtpClienException("Couldn't connect to remote server", ex);
            }
            readResponse();
            if (_statusCode != 220)
            {
                CloseConnection();
                throw new FtpClienException(result.Substring(4));
            }
            ExecuteCommand("USER " + _ftpUsername);
            if (!(_statusCode == 331 || _statusCode == 230))
            {
                LogOut();
                throw new FtpClienException(result.Substring(4));
            }
            if (_statusCode != 230)
            {
                ExecuteCommand("PASS " + _ftpPassword);
                if (!(_statusCode == 230 || _statusCode == 202))
                {
                    LogOut();
                    throw new FtpClienException(result.Substring(4));
                }
            }
            _isLoggedIn = true;
            ChangeWorkingDirectory(_ftpCurrentPath);
        }
        #endregion

        #region DownloadFile
        /// <summary>
        /// Download a remote file to a local file name which can include
        /// a path, and set the resume flag. The local file name will be
        /// created or overwritten, but the path must exist.
        /// </summary>
        /// <param name="ftpFile">File on the server to download</param>
        /// <param name="localFile">Name of the file on the local machine</param>
        /// <param name="resume"></param>
        public void DownloadFile(string ftpFile, string localFile, bool resume)
        {
            if (!_isLoggedIn)
            {
                throw new FtpClienException("You need to log in before you can perform this operation");
            }

            IsBinary = true;
            if (localFile.Equals(""))
            {
                localFile = ftpFile;
            }
            FileStream output = null;
            if (!File.Exists(localFile))
            {
                output = File.Create(localFile);
            }
            else
            {
                output = new FileStream(localFile, FileMode.Open);
            }
            Socket dataSocket = OpenSocketForTransfer();
            long resumeOffset = 0;
            if (resume)
            {
                resumeOffset = output.Length;
                if (resumeOffset > 0)
                {
                    ExecuteCommand("REST " + resumeOffset);
                    if (_statusCode != 350)
                    {
                        resumeOffset = 0;
                        throw new FtpClienException("Resuming not supported:" + result.Substring(4));
                    }
                   output.Seek(resumeOffset, SeekOrigin.Begin);
                    
                }
            }
            ExecuteCommand("RETR " + ftpFile);
            if (_statusCode != 150 && _statusCode != 125)
            {
                throw new FtpClienException(result.Substring(4));
            }
            DateTime timeout = DateTime.Now.AddSeconds(_timeOut);
            while (timeout > DateTime.Now)
            {
                bytesResponse= dataSocket.Receive(buffer, buffer.Length, 0);
                output.Write(buffer, 0, bytesResponse);
                if (bytesResponse<= 0)
                {
                    break;
                }
            }
            output.Close();
            if (dataSocket.Connected)
            {
                dataSocket.Close();
            }
            readResponse();
            if (_statusCode != 226 && _statusCode != 250)
            {
                throw new FtpClienException(result.Substring(4));
            }
        }
        #endregion

        #region UploadFile
        /// <summary>
        /// Upload a file and set the resume flag.
        /// </summary>
        /// <param name="fileName"></param>
        /// <param name="resume"></param>
        public void UploadFile(string fileName, bool resume)
        {
            if (!_isLoggedIn)
            {
                throw new FtpClienException("You need to log in before you can perform this operation");
            }

            Socket dataSocket = null;
            long resumeOffset = 0;
            if (resume)
            {
                try
                {
                    IsBinary = true;
                    resumeOffset = GetFileSize(Path.GetFileName(fileName));
                }
                catch (Exception)
                {
                    resumeOffset = 0;
                }
            }
            FileStream input = new FileStream(fileName, FileMode.Open);
            if (resume && input.Length < resumeOffset)
            {
                resumeOffset = 0;
            }
            else if (resume && input.Length == resumeOffset)
            {
                input.Close();
                Debug.WriteLine("Skipping completed " + fileName + " - turn resume off to not detect.", "FtpClient");
                return;
            }
            dataSocket = OpenSocketForTransfer();
            if (resumeOffset > 0)
            {
                ExecuteCommand("REST " + resumeOffset);
                if (_statusCode != 350) resumeOffset = 0;
                
            }
            ExecuteCommand("STOR " + Path.GetFileName(fileName));
            if (_statusCode != 125 && _statusCode != 150)
                throw new FtpClienException(result.Substring(4));
            if (resumeOffset != 0)
                input.Seek(resumeOffset, SeekOrigin.Begin);

            while ((bytesResponse= input.Read(buffer, 0, buffer.Length)) > 0)
            {
                dataSocket.Send(buffer, bytesResponse, 0);
            }
            input.Close();
            if (dataSocket.Connected)
            {
                dataSocket.Close();
            }
            readResponse();
            if (_statusCode != 226 && _statusCode != 250)
            {
                throw new FtpClienException(result.Substring(4));
            }
        }
        #endregion

        #region UploadDirectory
        /// <summary>
        /// Upload a directory and its file contents
        /// </summary>
        /// <param name="dirPath">Path of the directory to upload</param>
        /// <param name="recursive">Whether to recurse sub directories</param>
        /// <param name="extension">Only upload files of the given extension(i.e;'*.*','*.jpg', ect..)</param>
        public void UploadDirectory(string dirPath, bool recursive, string extension)
        {
            if (!_isLoggedIn)
            {
                throw new FtpClienException("You need to log in before you can perform this operation");
            }
            string[] directories = dirPath.Replace("/", @"\").Split('\\');
            string rootDirectory = directories[directories.Length - 1];

            if (ListFiles(rootDirectory).Length < 1)
            {
                CreateDirectory(rootDirectory);
            }
            ChangeWorkingDirectory(rootDirectory);
            foreach (string file in Directory.GetFiles(dirPath, extension))
            {
                UploadFile(file, true);
            }
            if (recursive)
            {
                foreach (string directory in Directory.GetDirectories(dirPath))
                {
                    UploadDirectory(directory, recursive, extension);
                }
            }
            ChangeWorkingDirectory("..");
        }
        #endregion

        #region DeleteFile
        /// <summary>
        /// method to delete a file from the FTP server.
        /// </summary>
        /// <param name="file">File to delete</param>
        public void DeleteFile(string file)
        {
            if (!_isLoggedIn)
            {
                throw new FtpClienException("You need to log in before you can perform this operation");
            }
            ExecuteCommand("DELE " + file);
            if (_statusCode != 250)
            {
                throw new FtpClienException(result.Substring(4));
            }
        }
        #endregion


        #region CloseConnection
        /// <summary>
        /// method to close the connection
        /// </summary>
        public void CloseConnection()
        {
            if (ftpSocket != null)
            {
                ExecuteCommand("QUIT");
            }
            LogOut();
        }
        #endregion

        #region ListFiles
        /// <summary>
        /// 
        /// </summary>
        /// <param name="extension">Files extension</param>
        /// <returns>Return a string array containing the remote directory's file list.</returns>
        public string[] ListFiles(string extension)
        {
            if (!_isLoggedIn)
            {
                throw new FtpClienException("You need to log in before you can perform this operation");
            }
            Socket dataSocket = OpenSocketForTransfer();
            ExecuteCommand("NLST " + extension);
            if (!(_statusCode == 150 || _statusCode == 125))
            {
                throw new FtpClienException(result.Substring(4));
            }
            statusMessage = "";
            DateTime timeout = DateTime.Now.AddSeconds(_timeOut);
            while (timeout > DateTime.Now)
            {
                int bytesResponse = dataSocket.Receive(buffer, buffer.Length, 0);
                statusMessage += ASCII.GetString(buffer, 0, bytesResponse);
                if (bytesResponse < buffer.Length) break;
            }
            string[] msg = statusMessage.Replace("\r", "").Split('\n');
            dataSocket.Close();
            if (statusMessage.IndexOf("No such file or directory") != -1)
                msg = new string[] { };
            readResponse();
            if (_statusCode != 226)
                msg = new string[] { };
            return msg;
        }
        #endregion

        #region GetFileSize
        /// <summary>
        /// Method to retrieve the size of the file based
        /// on the name provided
        /// </summary>
        /// <param name="file">Name of the file to get the size of</param>
        /// <returns>The files size</returns>
        public long GetFileSize(string file)
        {
            if (!_isLoggedIn)
            {
                throw new FtpClienException("You need to log in before you can perform this operation");
            }
            ExecuteCommand("SIZE " + file);
            long fileSize = 0;
            if (_statusCode == 213)
            {
                fileSize = long.Parse(result.Substring(4));
            }
            else
            {
                throw new FtpClienException(result.Substring(4));
            }
            return fileSize;
        }
        #endregion

        #region RenameFile
        /// <summary>
        /// Rename a file on the remote FTP server.
        /// </summary>
        /// <param name="oldName">File to rename</param>
        /// <param name="newName">New name of the file</param>
        /// <param name="replace">setting to false will throw exception if it exists</param>
        public void RenameFile(string oldName, string newName, bool replace = true)
        {
            if (!_isLoggedIn)
            {
                throw new FtpClienException("You need to log in before you can perform this operation");
            }
            ExecuteCommand("RNFR " + oldName);
            if (_statusCode != 350)
            {
                throw new FtpClienException(result.Substring(4));
            }
            if (!replace && ListFiles(newName).Length > 0)
            {
                throw new FtpClienException("File already exists");
            }
            ExecuteCommand("RNTO " + newName);
            if (_statusCode != 250)
            {
                throw new FtpClienException(result.Substring(4));
            }
         }
        #endregion

        #region CreateDirectory
        /// <summary>
        /// Create a directory on the remote FTP server.
        /// </summary>
        /// <param name="dirName">Name of the directory to create</param>
        public void CreateDirectory(string dirName)
        {
            if (!_isLoggedIn)
            {
                throw new FtpClienException("You need to log in before you can perform this operation");
            }
            if (dirName == null || dirName.Equals(".") || dirName.Length == 0)
            {
                throw new FtpClienException("A directory name wasn't provided. Please provide one and try your request again.");
            }
            ExecuteCommand("MKD " + dirName);
            if (_statusCode != 250 && _statusCode != 257)
            {
                throw new FtpClienException(result.Substring(4));
            }
        }
        #endregion

        #region RemoveDirectory
        /// <summary>
        /// Delete a directory on the remote FTP server.
        /// </summary>
        /// <param name="dirName">Name of directory to remove</param>
        public void RemoveDirectory(string dirName)
        {
            if (!_isLoggedIn)
            {
                throw new FtpClienException("You need to log in before you can perform this operation");
            }
            if (dirName == null || dirName.Equals(".") || dirName.Length == 0)
            {
                throw new FtpClienException("A directory name wasn't provided. Please provide one and try your request again.");
            }
            ExecuteCommand("RMD " + dirName);
            if (_statusCode != 250)
            {
                throw new FtpClienException(result.Substring(4));
            }
        }
        #endregion

        #region ChangeWorkingDirectory
        /// <summary>
        /// Change the current working directory on the remote FTP server.
        /// </summary>
        /// <param name="dirName"></param>
        public void ChangeWorkingDirectory(string dirName)
        {
            if (dirName == null || dirName.Equals(".") || dirName.Length == 0)
            {
                throw new FtpClienException("A directory name wasn't provided. Please provide one and try your request again.");
            }
            if (!_isLoggedIn)
            {
                throw new FtpClienException("You need to log in before you can perform this operation");
            }
            ExecuteCommand("CWD " + dirName);
            if (_statusCode != 250)
            {
                throw new FtpClienException(result.Substring(4));
            }
            ExecuteCommand("PWD");
            if (_statusCode != 257)
            {
                throw new FtpClienException(result.Substring(4));
            }
            _ftpCurrentPath = statusMessage.Split('"')[1];
        }
        #endregion

        #region readResponse
        /// <summary>
        /// 
        /// </summary>
        private void readResponse()
        {
            statusMessage = "";
            result = ParseHostResponse();
            _statusCode = int.Parse(result.Substring(0, 3));
        }
        #endregion

        #region ParseHostResponse
        /// <summary>
        /// Method to parse the response from the remote host
        /// </summary>
        /// <returns>this method return the status Message of the response</returns>
        private string ParseHostResponse()
        {
            do
            {
                bytesResponse = ftpSocket.Receive(buffer, buffer.Length, 0);
                statusMessage += ASCII.GetString(buffer, 0, bytesResponse);
            } while (bytesResponse >= buffer.Length);
            string[] msg = statusMessage.Split('\n');
            if (statusMessage.Length > 2)
                statusMessage = msg[msg.Length - 2];
            else
                statusMessage = msg[0];
            if (!statusMessage.Substring(3, 1).Equals(" ")) return ParseHostResponse();
            return statusMessage;
        }
        #endregion

        #region ExecuteCommand
        /// <summary>
        /// method to send the ftp commands to the remote server
        /// </summary>
        /// <param name="command">the command to execute</param>
        protected void ExecuteCommand(String command)
        {
     
            Byte[] BytesCommand = Encoding.ASCII.GetBytes((command + "\r\n").ToCharArray());
            ftpSocket.Send(BytesCommand, BytesCommand.Length, 0);
            readResponse();
        }
        #endregion

        #region OpenSocketForTransfer
        /// <summary>
        /// when doing data transfers, we need to open another socket for it.
        /// </summary>
        /// <returns>Connected socket</returns>
        private Socket OpenSocketForTransfer()
        {
            ExecuteCommand("PASV");
            if (_statusCode != 227)
            {
                throw new FtpClienException(result.Substring(4));
            }
            int idx1 = result.IndexOf('(');
            int idx2 = result.IndexOf(')');
            string ipData = result.Substring((idx1 + 1), (idx2 - idx1) - 1);
            int[] msgSegments = new int[6];
            int msgLength = ipData.Length;
            int partCount = 0;
            string buffer = "";
            for (int i = 0; i < msgLength && partCount <= 6; i++)
            {
                char chr = char.Parse(ipData.Substring(i, 1));
                if (char.IsDigit(chr))
                {
                    buffer += chr;
                }
                else if (chr != ',')
                {
                    throw new FtpClienException("Malformed PASV result: " + result);
                }
                else
                {
                    if (chr == ',' || i + 1 == msgLength)
                    {
                        try
                        {
                            msgSegments[partCount++] = int.Parse(buffer);
                            buffer = "";
                        }
                        catch (Exception ex)
                        {
                            throw new FtpClienException("Malformed PASV result (not supported?): " + result, ex);
                        }
                    }
                }
            }
            string ipAddress = msgSegments[0] + "." + msgSegments[1] + "." + msgSegments[2] + "." + msgSegments[3];
            int port = (msgSegments[4] << 8) + msgSegments[5];
            Socket tranferSocket = null;
            IPEndPoint ipEndPoint = null;
            try
            {
                tranferSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
                ipEndPoint = new IPEndPoint(Dns.GetHostEntry(ipAddress).AddressList[0], port);
                tranferSocket.Connect(ipEndPoint);
            }
            catch (Exception ex)
            {
                if (tranferSocket != null && tranferSocket.Connected) tranferSocket.Close();
                throw new FtpClienException("Can't connect to remote server", ex);
            }
            return tranferSocket;
        }

        #endregion

        #region LogOut
        /// <summary>
        /// method to close and remove any sockets opened
        /// </summary>
        private void LogOut()
        {
            if (ftpSocket != null)
            {
                ftpSocket.Close();
                ftpSocket = null;
            }
            _isLoggedIn = false;
        }
        #endregion

        #region Destructor
        /// <summary>
        /// Destuctor
        /// </summary>
        ~FtpClient()
        {
            LogOut();
        }
        #endregion
      #endregion
    }
}